package main

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"github.com/prometheus/client_golang/prometheus"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

const (
	RedfishAPI = "https://%s/redfish/v1"
)

var (
	DiskStatus = map[string]float64{
		"Ready":             1,  // Ready
		"Unconfigured_Good": 2,  // 未配置
		"Unconfigured_Bad":  3,  //配置异常
		"Hot_Spare":         4,  // 热备盘
		"Offline":           5,  // 离线
		"FAILED":            6,  // 故障
		"Rebuilding":        7,  //重建
		"Optimal":           8,  // Optimal
		"Online":            9,  // 已配置
		"JBOD":              10, //JBOD
		"Unknown":           11, //未知
		"Predict_Fail":      12,
		"Copyback":          13,
		"PFA":               14, //预故障
		"Raw":               15, //RAW状态
		"Hotspare":          16, //热备盘
		"RebuildWait":       17,
		// NVMe 盘
		"Spare_below":     18,
		"Temp_Anomaly":    19,
		"Subsys_Degraded": 20,
		"Read_only":       21,
		"Cache_Failed":    22,
		"Normal":          23,
	}
	client = &http.Client{Transport: &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
		Timeout: time.Second * 60,
	}
)

type Response struct {
	AuthToken string
}

func newHttpRequest(method, url string, reqBody interface{}) (*http.Request, error) {
	var byteData []byte
	var err error
	if reqBody != nil {
		byteData, err = json.Marshal(reqBody)
		if err != nil {
			return nil, err
		}
	}
	request, err := http.NewRequest(method, url, bytes.NewReader(byteData))
	if err != nil {
		return nil, err
	}
	request.Header.Set("Content-Type", "application/json;charset=utf-8")
	return request, nil
}

func doHttpRequest(request *http.Request) ([]byte, http.Header, error) {
	resp, err := client.Do(request)
	if err != nil {
		return nil, nil, err
	}
	if !(resp.StatusCode == 200 || resp.StatusCode == 201) {
		return nil, nil, err
	}
	respBody, err := ioutil.ReadAll(resp.Body)
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(resp.Body)
	if err != nil {
		return nil, nil, err
	}
	return respBody, resp.Header, nil
}

func RedfishAPIAvailable(host string) bool {
	req, err := newHttpRequest(http.MethodGet, fmt.Sprintf("https://%s/redfish", host), nil)
	if err != nil {
		return false
	}
	_, _, err = doHttpRequest(req)
	if err != nil {
		return false
	}
	return true
}

type Session struct {
	ODataID string `json:"@odata.id"`
	Id      string
}

func CreateRedfishSession(host, username, password string) (string, string, error) {
	url := fmt.Sprintf(RedfishAPI+"/SessionService/Sessions", host)
	body := map[string]string{
		"UserName": username,
		"Password": password,
	}
	req, err := newHttpRequest(http.MethodPost, url, body)
	if err != nil {
		return "", "", err
	}
	respBody, respHeader, err := doHttpRequest(req)
	if err != nil {
		return "", "", err
	}
	var session Session
	err = json.Unmarshal(respBody, &session)
	if err != nil {
		return "", "", err
	}
	return session.ODataID, respHeader.Get("X-Auth-Token"), nil
}

func DeleteRedfishSession(host, sessionId, token string) error {
	url := fmt.Sprintf("https://%s%s", host, sessionId)
	request, err := newHttpRequest("DELETE", url, nil)
	if err != nil {
		return err
	}
	request.Header.Set("X-Auth-Token", token)
	respBody, respHeader, err := doHttpRequest(request)
	print(respBody)
	print(respHeader)
	return err
}

type Drives struct {
	Members []Disk `json:"Members"`
}

type Disk struct {
	DataId string `json:"@odata.id"`
}

type DiskFront struct {
	CapableSpeedGbs    float64 `json:"CapableSpeedGbs,omitempty"`
	CapacityBytes      int64   `json:"CapacityBytes,omitempty"`
	HotspareType       string  `json:"HotspareType,omitempty"`
	ID                 string  `json:"Id,omitempty"`
	IndicatorLED       string  `json:"IndicatorLED,omitempty"`
	Manufacturer       string  `json:"Manufacturer,omitempty"`
	MediaType          string  `json:"MediaType,omitempty"`
	Model              string  `json:"Model,omitempty"`
	Name               string  `json:"Name,omitempty"`
	NegotiatedSpeedGbs float64 `json:"NegotiatedSpeedGbs,omitempty"`
	Oem                struct {
		Public struct {
			ConnectionID      int64       `json:"ConnectionID"`
			DriveNumberInBios interface{} `json:"DriveNumberInBios"`
			DriveNumberInOS   interface{} `json:"DriveNumberInOS"`
			FirmwareStatus    string      `json:"FirmwareStatus"`
			HoursOfPoweredUp  int         `json:"HoursOfPoweredUp"`
			OwnerVolume       struct {
				Name   string `json:"Name"`
				Status struct {
					Health string `json:"Health"`
					State  string `json:"State"`
				} `json:"Status"`
				VolumeRaidLevel string `json:"VolumeRaidLevel"`
			} `json:"OwnerVolume,omitempty"`
			Panel string `json:"Panel"`
		} `json:"Public"`
	} `json:"Oem"`
	Protocol     string `json:"Protocol"`
	SerialNumber string `json:"SerialNumber"`
	Status       struct {
		Health string `json:"Health"`
		State  string `json:"State"`
	} `json:"Status"`
}

func GetDiskMetrics(host, path, token string) (prometheus.Metric, error) {
	diskUrl := fmt.Sprintf("https://%s%s", host, path)
	req, err := newHttpRequest(http.MethodGet, diskUrl, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Auth-Token", token)
	respBody, _, err := doHttpRequest(req)
	if err != nil {
		return nil, err
	}
	var diskFront DiskFront
	err = json.Unmarshal(respBody, &diskFront)
	if err != nil {
		return nil, err
	}
	pvMetrics, err := prometheus.NewConstMetric(
		prometheus.NewDesc("ipmi_physical_volume_status", "physical volume info from h3c redfish",
			[]string{"panel", "type", "capacity"}, nil),
		prometheus.GaugeValue,
		DiskStatus[diskFront.Oem.Public.FirmwareStatus],
		diskFront.Oem.Public.Panel,
		fmt.Sprintf("%d Gbs %s %s", diskFront.NegotiatedSpeedGbs, diskFront.Protocol, diskFront.MediaType),
		fmt.Sprintf("%d GiB", diskFront.CapacityBytes/1024/1024/1024),
	)
	return pvMetrics, nil
}

func GetDriveMetrics(host, token string) ([]prometheus.Metric, error) {
	url := fmt.Sprintf(RedfishAPI+"/Chassis/1/Drives", host)
	req, err := newHttpRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Auth-Token", token)
	respBody, _, err := doHttpRequest(req)
	if err != nil {
		return nil, err
	}
	var drives Drives
	err = json.Unmarshal(respBody, &drives)
	if err != nil {
		return nil, err
	}
	var diskMetrics []prometheus.Metric
	for _, disk := range drives.Members {
		pvMetrics, err := GetDiskMetrics(host, disk.DataId, token)
		if err != nil {
			return nil, err
		}
		diskMetrics = append(diskMetrics, pvMetrics)
	}
	return diskMetrics, nil
}

func ScrapeTarget(host, username, password string) ([]prometheus.Metric, error) {
	sessionIdPath, token, err := CreateRedfishSession(host, username, password)
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = DeleteRedfishSession(host, sessionIdPath, token)
	}()
	diskMetrics, err := GetDriveMetrics(host, token)
	if err != nil {
		return nil, err
	}
	return diskMetrics, nil
}

func main() {
	diskMetrics, err := ScrapeTarget("10.254.4.89", "admin", "Password@_")
	if err != nil {
		log.Fatal(err)
	}
	print(diskMetrics)
}
